Repository navigation
Windows support: WSL boundary, working agent dispatch, doctor, sandbox badge, and CI that would have caught it - #465
Merged
currentbits merged 25 commits intoAug 18, 2026
Conversation
git, watchers and agents must run on the same side of the WSL boundary as the worktree. A repo reached over \\wsl$ from Windows is ~10x slower under Windows git and drops inotify events entirely. electron/wsl.js detects which side a project path lives on; ssh.js's wrapCommand — already the single wrap seam for ssh remotes — grows a second branch that turns a WSL-side project into `wsl.exe -d <distro> --cd <path> -- <argv>`. runner.js's resolveSpawn and the two remoteHost branches around it move to one crossesBoundary() predicate so ssh and WSL stay in step. Pure detection, no spawning, platform injectable — the suite runs the win32 cases on macOS. Off win32 every path is "unix" and nothing changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…into coder/fork-windows-support-hardening-wsl-bound-e59f7d
…into coder/fork-windows-support-hardening-wsl-bound-a11a21
…into coder/fork-windows-support-hardening-wsl-bound-ab4875
…into coder/fork-windows-support-hardening-wsl-bound-93db45
Show whether the next run is actually confined (agent × repo location) as a header badge. The resolver reports what we pass today: only Codex is sandboxed (its default CLI sandbox); everyone else is an honest no.
On win32, projects.add runs four advisory checks (core.longpaths, Git Bash, Node 22, WSL boundary) through the wrap seam and returns them on the project. Failed checks never reject the add. Off win32 the doctor is a no-op. The add-project modal lists what's wrong and what to do; Continue dismisses it.
Add windows-latest to the test job so win32 paths stay honest. Make npm scripts portable ( --prefix, --import for CODER_GROK_MCP_DISABLE ) instead of pinning the Windows job to bash. Electron on win32 only runs the WSL contract: the rest of that suite spawns shebang files as CODER_*_BIN, and CreateProcess will not execute them. There is no node-pty in the repo, so a ConPTY rebuild step would be a no-op. Winget publishes an installer. package-cross.sh produces an unsigned portable zip, not one — do not add a manifest that points at it.
…into coder/windows-support-hardening-wsl-boundary-d-d3d627
userData is a Windows path, so a WSL-side repo's worktree used to land on the wrong filesystem — and git worktree add running inside the distro cannot address a UNC. Park those worktrees next to the repo on ext4 (<repoParent>/.solenta/worktrees/<id>, or /tmp if the repo is on /mnt/<drive>) and store the matching UNC so Windows-side existsSync still works. Git helpers now share one wrap: a WSL UNC cwd becomes wsl.exe --cd, and UNC path args (worktree add/remove) are rewritten to the linux path. Non-win32 behaviour is unchanged. No file watcher exists today; status/diff are on-demand and PR refresh is already interval-polled.
Sequentially they stack three PROBE_TIMEOUT_MS waits, so a box with no bash and no node froze the add dialog for 15s — on exactly the platform the doctor exists for. The probes are independent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…into coder/windows-support-hardening-wsl-boundary-d-d3d627
…into coder/windows-support-hardening-wsl-boundary-d-d3d627
The win32 file list was written before the doctor and sandbox suites existed — parallel workers could not see each other. All three new files are pure resolvers or ssh.setExecFile fakes, so they run on Windows, and they are precisely the win32 logic the matrix exists to keep honest. Windows now runs 44 tests instead of 7. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two unrelated-looking bugs in the same function. 1. isBinAvailable shelled out to `which`, which does not exist on Windows, so every provider failed "Provider binary not found" and nothing could be dispatched. win32 uses `where`, which prints every match in PATH order — take the first line. Only the local host is in question here: a project across a boundary returns early in assertProviderBinary, because the CLI lives on the other side. The cache key now includes the platform so a win32 miss cannot poison a posix hit. 2. That cache key held a raw NUL byte as its separator, which made `file` report providers.js as binary and made every plain `grep` of this file print nothing and exit 1 — indistinguishable from a real no-match. It has cost two sessions a wrong conclusion (#441). Now the \0 escape: same string, plain-text file. Agent CLIs install on Windows as .cmd shims and Node refuses to exec those. The six sites that spawn a provider binary go through cross-spawn, which routes them via cmd.exe with correct escaping — necessary because the prompt travels in argv, so hand-rolled quoting would be a correctness and injection risk. Not yet Windows-ready: /bin/sh in verify.js, `which node` in memory-sup, and the SHELL default in pathEnv. Nothing here proves the app boots. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…into coder/fork-windows-support-hardening-wsl-bound-ea7084 # Conflicts: # src/components/ThreadView.tsx
…into coder/fork-windows-support-hardening-wsl-bound-4080d8 # Conflicts: # src/components/ThreadView.tsx
…into coder/fork-windows-support-hardening-wsl-bound-89b04f # Conflicts: # src/components/ThreadView.tsx
Follow-up to #442. The four leftover POSIX-only spots now have a win32 path or an honest no-op, all testable on macOS by injecting the platform. 1. verify.js: Windows has no /bin/sh. Spawn Git Bash (`bash -c`) — the same POSIX shell the doctor already probes for. Verify commands are POSIX (`>&2`, `exit 3`); cmd.exe or PowerShell would silently mis-parse them. Missing bash is a real spawn error, not a fallback. A WSL-side project is wrapped through ssh.js so the command runs inside the distro. SSH remotes are left unwrapped: wrapping them would change macOS remote-verify behaviour. 2. memory-sup.js: `which node` is now providers.defaultWhich, so win32 gets `where`. Two call sites, one existing helper; no new abstraction. 3. pathEnv.js: there is no login shell of the macOS/launchd kind on Windows. captureLoginPath and enrichProcessPath are explicit no-ops (source: "win32") instead of spawning /bin/zsh and failing closed. Also: do not split PATH on ':'. That would shred `C:\Windows\system32`. 4. devservers.js never used /bin/sh (argv is `npm run <script>`). It now goes through cross-spawn so npm.cmd works, wraps WSL-side roots the same way as verify, and kills the pid directly on win32 (no POSIX process group). wrapCommand accepts an optional platform so WSL wrapping is testable off Windows. Non-win32 behaviour is unchanged.
Nothing in CI has ever started the app on Windows, which is how a dead dispatch path survived. smoke.js is already a full boot + IPC + fake-CLI harness; it just could not run on win32. resolveNodeBinary() now uses defaultWhich (where on win32, which elsewhere) — same helper as #442. Fake CLIs stay shebang scripts so the macOS path is unchanged. On win32 a sibling .cmd runs `node <script> %*` and that path is what CODER_*_BIN points at, because CreateProcess will not execute a shebang file and CODER_*_BIN must stay one executable token (cross-spawn can launch the .cmd). A new `smoke` job, separate from `npm test`, runs on macos-latest and windows-latest after vite + core builds. npm run test:smoke is `electron electron/smoke.js`. fail() already called app.exit(1); a top-level uncaughtException handler was added because Electron stays alive after "App threw an error during load", which would hang CI. macOS: all five passes, exit 0. Missing dist: exit 1. Windows is unverified until this job runs in CI.
…into coder/windows-support-hardening-wsl-boundary-d-d3d627
One writeFakeBin helper replaces 32 local shebang writers. POSIX still writes the same #!/usr/bin/env node file. Win32 adds a .cmd wrapper that cross-spawn can launch. The windows-latest list grows from 4 files / 44 tests to 21 / 238. gh and fm stay POSIX-only: they still execFile the fake, which cannot run .cmd.
…into coder/windows-support-hardening-wsl-boundary-d-d3d627
Parallel workers landed one in electron/smoke.js and one in electron/test/support/fakeBin.js. Same name, opposite rule for which node the .cmd wrapper invokes: the test suite runs under plain node so process.execPath is right there, smoke runs under Electron where it is electron.exe and would boot a second Electron per fake. Both correct, neither safe to copy into the other. Say so in both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two conflicts, both from the context-ring work (#317) landing on main while this branch was open: - ThreadView.tsx: git aligned the new SandboxBadge (#436) against main's rewritten ContextRingBadge. Both are kept — main's version of the ring already appears in full further down, so its side of the hunk was a duplicate. - context-usage.test.js: main still had the local shebang writer; this branch migrated it to the shared writeFakeBin helper (#450). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
currentbits
deleted the
coder/windows-support-hardening-wsl-boundary-d-d3d627
branch
October 3, 2026 16:07
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #397, #434, #435, #436, #437, #441, #442, #448, #449, #450.
Why
#397 asked to harden Windows support. Partway through it became clear there was no working baseline to harden:
isBinAvailableshelled out towhich, which does not exist on Windows, soassertProviderBinarythrew for every provider and no agent could be dispatched at all. The window opened, the UI worked, and the app fell over on the first run.That bug survived because nothing in CI had ever started the app on Windows. So this PR does the hardening and removes the conditions that hid the problem.
What changed
The WSL boundary (#397, #434). On Windows a repo lives either on a Windows drive or inside a WSL distro, reachable as
\\wsl$\<distro>\.... Crossing that boundary per file op is the main source of slow git and broken watching. The rule is that git, watchers and agents run on the same side as the worktree. Rather than build a second mechanism, this extendsssh.js'swrapCommand— already the single wrap seam for ssh remotes — with a WSL branch producingwsl.exe -d <distro> --cd <path> -- <argv>.runner.js's three scatteredremoteHostchecks collapse into onecrossesBoundary(). Worktrees for a WSL-side repo now land inside the distro rather than under WindowsuserData, wheregit worktree addrunning in the distro could not even address them.Agent dispatch (#442).
which→whereon win32, taking the first line sincewhereprints every match; the lookup cache is now keyed by platform so a win32 miss cannot poison a posix hit. The six sites that spawn a provider binary go throughcross-spawn, because Windows installs these CLIs as.cmdshims that Node refuses to exec — and the prompt travels in argv, so hand-rolledcmd.exequoting would be a correctness and injection risk. No WSL special-casing was needed here: a project across a boundary already returns early, since the CLI lives on the other side.Remaining POSIX assumptions (#448).
verify.jsuses Git Bash (bash -c) on win32 rather than cmd.exe or PowerShell, because verify commands are POSIX and the alternatives would silently mis-parse them; a missing bash is a real error, not a fallback.pathEnv.jsis an explicit win32 no-op and no longer splitsPATHon:, which would have shreddedC:\Windows\system32— silent corruption, not a crash.devservers.jshandlesnpm.cmdand kills by pid on win32.Windows doctor (#435). On project add, four advisory probes —
core.longpaths, POSIX shell, Node 22, and which side of the boundary the repo is on — routed through the wrap seam so a WSL-side repo is probed inside the distro. Never blocks the add; no-op off win32.Per-thread sandbox badge (#436). Confinement depends on agent × repo location, so it is resolved per thread. Only Codex comes out sandboxed (its own default CLI sandbox); everyone else is an honest no, with the reason on hover.
CI (#437, #449, #450).
windows-latestadded to the test matrix. A separatesmokejob runselectron/smoke.js— a real Electron boot with real IPC and real spawn paths — on macOS and Windows, so a boot failure reads as a boot failure. One sharedwriteFakeBinreplaces 32 local shebang writers, taking the Windows leg from 44 tests to 238.#441, a literal NUL byte in
providers.js, is fixed here too: it sat in the very cache key this PR rewrites, and it had made every plaingrepof that file silently return nothing.Verification
macOS: electron 1253/1253, renderer 937/937, memory 173/173, core 38/38, typecheck clean, smoke boots and exits 0 (and exits 1 when broken — I checked, since a smoke job that cannot fail is worthless).
Windows itself is unverified. Nothing on a Mac can prove it; the new smoke job answers that on this PR's first CI run. I would expect the display on the runner and
git.exein the worktree tests to be the first things to shake out.Not included
Windows packaging and winget (Windows packaging: a signable installer winget can publish #440) — blocked on an Authenticode signing decision, not on code. Deliberately no manifest pointing at an unsigned zip.
Linux (Linux: prove it works — no CI leg, never tested #455) — untouched and untested, no CI leg. POSIX means the Windows blockers do not apply, but that is inference, and inference is exactly what was wrong about Windows.
node-pty/ConPTY from the original issue: there is no node-pty in this repo, so it was a no-op.
PR created by the Claude Code agent